feat(billing): ship pricing constants contract + refundCharge service#3775
Conversation
…fig/defaults/
Promote billing.pricing.constants shape to devkit (Task 1a of billing-residual-cleanup plan).
Adds config/defaults/billing.pricing.constants.js with the export contract + safe
defaults (PRICING_VERSION='0.0.0', PLAN_QUOTAS={free:0}, RATIOS={}, STRIPE_PRICE_CENTS={},
STRIPE_PACK_CENTS={}). Wires into development.config.js under billing.pricing so
importers can read via config.billing.pricing.*. Downstream projects override values
(not the shape) in their own project config; devkit ships the contract only.
…y key
Promote billing.refund.service.js from trawl to devkit (Task 3a of billing-residual-cleanup plan).
Wraps stripe.refunds.create with a deterministic idempotency key (refund_{chargeId}_{amount|full})
so retries on network failures never create duplicate refunds. Ledger debit happens via
the charge.refunded webhook (single source of truth) — this service only initiates the
Stripe refund. Full + partial refund, reason passthrough, empty chargeId and non-positive
amount guards all covered by 6 unit test cases.
|
Warning Review limit reached
More reviews will be available in 51 minutes and 22 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThis PR adds a billing pricing constants contract with safe defaults wired into development config, alongside a new Stripe refund service that supports full and partial refunds with input validation and idempotency. ChangesBilling Module Enhancements
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3775 +/- ##
==========================================
+ Coverage 90.18% 90.19% +0.01%
==========================================
Files 151 152 +1
Lines 5003 5020 +17
Branches 1589 1598 +9
==========================================
+ Hits 4512 4528 +16
- Misses 386 387 +1
Partials 105 105
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds two new billing building blocks intended to be consumed by downstream projects: (1) a devkit-shipped pricing constants “contract” wired into config.billing.pricing, and (2) a new refundCharge service that initiates Stripe refunds with a deterministic idempotency key. It also adds unit tests to verify the contract shape, config wiring, and refund behavior.
Changes:
- Add
config/defaults/billing.pricing.constants.jscontract exports and wire them intoconfig.billing.pricingviadevelopment.config.js. - Add
modules/billing/services/billing.refund.service.jswithrefundCharge()and unit tests for refund behaviors/idempotency. - Add contract + wiring unit tests to ensure the new config surface is reachable and has expected default shape.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
config/defaults/billing.pricing.constants.js |
Introduces pricing constants export contract with safe defaults. |
config/defaults/development.config.js |
Wires pricing constants into config.billing.pricing through the existing merge pipeline. |
lib/services/tests/billing-pricing-constants.contract.unit.tests.js |
Contract tests validating the exported constants’ shape/types. |
modules/billing/services/billing.refund.service.js |
Adds refundCharge() Stripe refund initiator with deterministic idempotency key. |
modules/billing/tests/billing.refund.service.refundCharge.unit.tests.js |
Unit tests for refundCharge() (full/partial, idempotency, validation). |
modules/billing/tests/billing.pricing.config.wiring.unit.tests.js |
Unit test verifying config.billing.pricing.* is accessible after config init. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/services/tests/billing-pricing-constants.contract.unit.tests.js`:
- Around line 11-27: Tighten the contract tests to assert the actual safe
defaults instead of broad types: change the assertions for RATIOS,
STRIPE_PRICE_CENTS, and STRIPE_PACK_CENTS to require they are empty objects
(e.g., deep-equal to {} or Object.keys(...).length === 0) so regressions that
populate them by default will fail; keep the existing checks for PRICING_VERSION
and PLAN_QUOTAS but consider also asserting PLAN_QUOTAS.free is a non-negative
number if relevant. Ensure you update the tests referencing the symbols RATIOS,
STRIPE_PRICE_CENTS, and STRIPE_PACK_CENTS only.
In `@modules/billing/services/billing.refund.service.js`:
- Line 21: The function refundCharge currently destructures the third parameter
with ({ reason } = {}) which throws a TypeError if callers pass null; update
refundCharge to guard the options argument before destructuring (e.g., normalize
the incoming param to an object or explicitly check for null/invalid values) and
then extract reason, or throw a clear validation error when a null/invalid
options is passed so callers get a meaningful message; reference the
refundCharge function and adjust its parameter handling or top-of-function
validation accordingly.
In `@modules/billing/tests/billing.pricing.config.wiring.unit.tests.js`:
- Around line 2-17: The test imports the environment-resolving entrypoint
(config/index.js) so it may not verify the default config; change the test to
import the specific defaults module (development.config.js) and run the same
assertions against that imported defaults object (e.g., replace the existing
import of config with a direct import of the development defaults and use
defaults.billing.pricing in the expect checks) so the test targets the new
billing.pricing block deterministically.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f26d6225-88d7-4d0f-a964-eb1e6df5ed68
📒 Files selected for processing (6)
config/defaults/billing.pricing.constants.jsconfig/defaults/development.config.jslib/services/tests/billing-pricing-constants.contract.unit.tests.jsmodules/billing/services/billing.refund.service.jsmodules/billing/tests/billing.pricing.config.wiring.unit.tests.jsmodules/billing/tests/billing.refund.service.refundCharge.unit.tests.js
…und service CR1: pin exact safe defaults in contract test (toEqual instead of typeof) so regressions that populate RATIOS/STRIPE_PRICE_CENTS/STRIPE_PACK_CENTS by default are caught. CR2: guard options param in refundCharge before destructuring — null arg caused raw TypeError; now throws a clear validation error. CR3: wiring test imports development.config.js directly (not config/index.js) so it deterministically targets the new billing.pricing block in the defaults file, not whichever env the full pipeline resolves.
All 3 actionable comments addressed in fix commit f719b3c: CR1 exact-default assertions, CR2 null-options guard, CR3 direct development.config.js import. CodeRabbit check itself is SUCCESS.
Summary
Combines T1a + T3a of plan
2026-06-02-trawl-billing-residual-cleanup.mdinto a single devkit PR to reduce PR count and wall-clock. Both additions are independent (zero call sites changed in devkit — pure new contract + new service). Trawl will absorb both via/update-stack+ swap importers in a separate trawl PR.T1a — Pricing constants contract (
config/defaults/)config/defaults/billing.pricing.constants.js— exports the shape contract with safe defaults:PRICING_VERSION='0.0.0',PLAN_QUOTAS={free:0},RATIOS={},STRIPE_PRICE_CENTS={},STRIPE_PACK_CENTS={}. JSDoc per export.config/defaults/development.config.js— imports the constants and exposes them atconfig.billing.pricing.*via the existing deep-merge pipeline. Downstream project configs override values (not shape) in their own file; devkit ships the contract only.lib/services/tests/billing-pricing-constants.contract.unit.tests.js— 5 shape assertions (types,freekey present).modules/billing/tests/billing.pricing.config.wiring.unit.tests.js— 2 assertions confirmingconfig.billing.pricing.*is reachable after full config init.T3a — refundCharge service (
modules/billing/services/)modules/billing/services/billing.refund.service.js— ported verbatim from trawl, de-trawlified (stripped issue refs). Wrapsstripe.refunds.createwith a deterministic idempotency key (refund_{chargeId}_{amount|full}). The actual ledger debit happens via thecharge.refundedwebhook (single source of truth) — this service only initiates the Stripe refund.modules/billing/tests/billing.refund.service.refundCharge.unit.tests.js— 6 cases: full refund, partial amount, deterministic idempotency, reason passthrough, empty chargeId rejection, non-positive amount rejection..refundCharge.infix (not the plan's prescribed.unit.suffix) becausebilling.refund.service.unit.tests.jsis already taken by the admin-controller refund tests (inlined logic from a prior simplification). The chosen name is unambiguous.Test plan
NODE_ENV=test npm run test:unit→ 1718 tests pass (0 failures)NODE_ENV=test npm run test:unit -- lib/services/tests/billing-pricing-constants.contractconfig.billing.pricingreachable:NODE_ENV=test npm run test:unit -- modules/billing/tests/billing.pricing.config.wiringNODE_ENV=test npm run test:unit -- modules/billing/tests/billing.refund.service.refundChargeconfig.billing.pricing+ delete trawl-local file) + T3b (/update-stackto absorb) in separate trawl PRs after this mergesSummary by CodeRabbit
New Features
Tests